﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Windows;

namespace PetriNetSharp.Engine
{
	/// <summary>
	/// A logical representation of a Petri Net graph
	/// </summary>
	/// <todo>
	/// Adding Arcs, Removing Arcs
	/// </todo>
	public class PetriNetGraph
	{
		#region constants

		/// <summary>
		/// The maximum the number portion of a UID can be. 
		/// </summary>
		private const int MAX_UID_NUMBER = 10000;

		/// <summary>
		/// The prefix in a transition UID specifying it is a transiton UID
		/// </summary>
		private const string TRANSITION_UID_PREFIX = "T";

		/// <summary>
		/// The prefix in a place UID specifying it is a place UID
		/// </summary>
		private const string PLACE_UID_PREFIX = "P";

		/// <summary>
		/// The default tokens to transfer for new transitions
		/// </summary>
		private const int DEFAULT_TOKENS_TO_TRANSFER = 1;

		#endregion

		/// <summary>
		/// Occurs when the Petri Net is stopped or started
		/// </summary>
		public event Action IsRunningChanged;

		/// <summary>
		/// Occurs when the Petri Net fires all its transitions at once
		/// </summary>
		public event Action Fired;

		/// <summary>
		/// Occurs just before the Petri Net fires all its transitions at once
		/// </summary>
		public event Action AboutToFire;

		/// <summary>
		/// Occurs when the graph is reset while running
		/// </summary>
		public event Action Resetted;

		/// <summary>
		/// The collection of Places in this PetriNet Graph
		/// </summary>
		private ObservableCollection<Place> places;

		/// <summary>
		/// The collection of Transitions in this PetriNet Graph
		/// </summary>
		private ObservableCollection<Transition> transitions;

		/// <summary>
		/// The collection of arcs in this PetriNet Graph
		/// </summary>
		private ObservableCollection<Arc> arcs;

		/// <summary>
		/// Whether the graph is currently running
		/// </summary>
		private bool isRunning;

		/// <summary>
		/// Create a new emtpy PetriNet Graph
		/// </summary>
		public PetriNetGraph()
		{
			places = new ObservableCollection<Place>();
			transitions = new ObservableCollection<Transition>();
			arcs = new ObservableCollection<Arc>();
			isRunning = false;
		}

		/// <summary>
		/// Get the collection of Places in this PetriNet Graph
		/// </summary>
		public ReadOnlyObservableCollection<Place> Places
			{ get { return new ReadOnlyObservableCollection<Place>(places); } }

		/// <summary>
		/// Get the collection of Transitions in this PetriNet Graph
		/// </summary>
		public ReadOnlyObservableCollection<Transition> Transitions
			{ get { return new ReadOnlyObservableCollection<Transition>(transitions); } }

		/// <summary>
		/// Get the collection of Arcs in this PetriNet Graph
		/// </summary>
		public ReadOnlyObservableCollection<Arc> Arcs
			{ get { return new ReadOnlyObservableCollection<Arc>(arcs); } }

		#region running functionality

		/// <summary>
		/// Get whether this PetriNet is running
		/// </summary>
		/// <remarks>
		/// Use Start() and Stop() to control
		/// </remarks>
		public bool IsRunning
		{
			get { return isRunning; }
			private set
			{
				if (isRunning != value)
				{
					isRunning = value;

					if(IsRunningChanged != null)
						IsRunningChanged();
				}
			}
		}

		/// <summary>
		/// Start the PetriNet
		/// </summary>
		/// <preconditions>
		/// IsRunning must be false
		/// </preconditions>
		public void Start()
		{
			#region preconditions
			Debug.Assert(!IsRunning, "Start called when the PetriNetGraph was already running",
				"Start() in PetriNetGraph was called when IsRunning is true");
			#endregion

			foreach (Place place in places)
				place.Start();

			foreach (Transition transition in transitions)
				transition.Start();

			IsRunning = true;
		}

		/// <summary>
		/// Stop the PetriNet
		/// </summary>
		/// <preconditions>
		/// IsRunning must be true
		/// </preconditions>
		public void Stop()
		{
			#region preconditions
			Debug.Assert(IsRunning, "Stop called when the PetriNetGraph was not running", 
				"Stop() in PetriNetGraph was called when IsRunning is false");
			#endregion

			IsRunning = false;

			foreach (Place place in places)
				place.Stop();

			foreach (Transition transition in transitions)
				transition.Stop();
		}

		/// <summary>
		/// Reset the graph so it is exactly how it was when it was started
		/// </summary>
		/// <preconditioins>
		/// IsRunning must be true
		/// </preconditioins>
		public void Reset()
		{
			#region preconditions
			Debug.Assert(IsRunning, "Reset called when IsRunning is false",
				"Reset() in PetriNetGraph was called when the graph was not currently running");
			#endregion

			foreach (Place place in places)
				place.ResetTokens();

			if (Resetted != null)
				Resetted();
		}

		#endregion

		#region add functionality

		/// <summary>
		/// Add a new Place to the PetriNet Graph
		/// </summary>
		/// <remarks>
		/// Creates a unique name for the Place
		/// </remarks>
		/// <param name="location">The location for the Place</param>
		/// <returns>The Place that is created</returns>
		public Place AddPlace(Point location)
		{
			string name = "P" + places.Count;
			Place place = new Place(location, name, GeneratePlaceUID());
			places.Add(place);
			return place;
		}

		/// <summary>
		/// Add an already made place to this graph
		/// </summary>
		/// <param name="place">The place to add</param>
		/// <preconditions>
		/// The place cannot already be in the graph
		/// </preconditions>
		public void AddPlace(Place place)
		{
			#region preconditions
			Debug.Assert(!places.Contains(place), "AddPlace called when place was already in PetriNet",
				"AddPlace was called when the parameter: place was already in the PetriNet");
			#endregion

			places.Add(place);
		}

		/// <summary>
		/// Add a new Transition to the PetriNet Graph
		/// </summary>
		/// <remarks>
		/// Creates a unique name for the Transition
		/// </remarks>
		/// <param name="location">The location for the Transition</param>
		/// <returns>The Transition that is created</returns>
		public Transition AddTransition(Point location)
		{
			string name = "T" + transitions.Count;
			Transition transition = new Transition(name, location, GenerateTransitionUID(), 
				DEFAULT_TOKENS_TO_TRANSFER);
			transitions.Add(transition);
			return transition;
		}

		/// <summary>
		/// Add an already made transition to this graph
		/// </summary>
		/// <param name="transition">The transition to add</param>
		/// <preconditions>
		/// The transition cannot already be in the graph
		/// </preconditions>
		public void AddTransition(Transition transition)
		{
			#region preconditions
			Debug.Assert(!transitions.Contains(transition), 
				"AddTransition called when transition was already in PetriNet",
				"AddTransition was called when the parameter: transition was already in the PetriNet");
			#endregion

			transitions.Add(transition);
		}

		/// <summary>
		/// Add an arc to this PetriNetGraph
		/// </summary>
		/// <remarks>
		/// This will attach the arc's place and transitions together
		/// </remarks>
		/// <param name="arc">The Arc to add</param>
		/// <preconditions>
		/// arc's transition must belong to the PetriNet Graph
		/// arc's place must belong to the PetriNetGraph
		/// </preconditions>
		public void AddArc(Arc arc)
		{
			#region preconditions

			Debug.Assert(transitions.Contains(arc.Transition), 
				"Arc was added whose transition was not part of PetriNet Graph",
				String.Format("AddArc was called with an Arc whose transition: {0} is not in the PetriNet Graph", arc.Transition.Name));

			Debug.Assert(places.Contains(arc.Place),
				"Arc was added whose place was not part of PetriNetGraph",
				String.Format("AddArc was called with an Arc whose place: {0} is not in the PetriNetGraph", arc.Place.Name));

			#endregion

			arcs.Add(arc);

			if (arc.IsInputArc)
				arc.Transition.ConnectInputPlace(arc.Place);
			else
				arc.Transition.ConnectOutputPlace(arc.Place);
		}

		#endregion

		#region delete functionality

		/// <summary>
		/// Deletes a Transitions from this PetriNet Graph
		/// </summary>
		/// <remarks>
		/// Also delete any Arcs connected to the passed Transition
		/// </remarks>
		/// <param name="transition">The Transition to delete</param>
		/// <preconditions>
		/// The Transition must be contained in this PetriNetGraph
		/// </preconditions>
		public void DeleteTransition(Transition transition)
		{
			#region preconditions
			Debug.Assert(this.transitions.Contains(transition), 
				String.Format("Transition {0} does not exist in this petri net graph", transition.Name), 
				"Cannot remove a tranisition from this PetriGraph if the tranisition is not contained in this PetriGraph");
			#endregion

			transitions.Remove(transition);
		}

		/// <summary>
		/// Deletes a Place from this PetriNet Graph
		/// </summary>
		/// <remarks>
		/// Also delete any Arcs connected to the passed Place
		/// </remarks>
		/// <preconditions>
		/// The Place must be contained in this PetriNetGraph
		/// </preconditions>
		/// <param name="transition">The Place to delete</param>
		public void DeletePlace(Place place)
		{
			#region preconditions
			Debug.Assert(this.places.Contains(place), 
				String.Format("Place {0} does not exist in this petri net graph", place.Name),
				"Cannot remove a place from this PetriGraph if the place is not contained in this PetriGraph");
			#endregion

			places.Remove(place);
		}

		/// <summary>
		/// Deletes an arc from this PetriNet Graph
		/// </summary>
		/// <param name="arc"></param>
		public void DeleteArc(Arc arc)
		{
			//Remove the arc's place from its transition
			arc.Transition.DisconnectPlace(arc.Place);

			//Remove connection from transition to arc
			arcs.Remove(arc);
		}

		#endregion

		#region fire functionality

		/// <summary>
		/// Get whether or not the graph can currently fire
		/// </summary>
		/// <returns>
		/// True if the graph can fire, false if not
		/// </returns>
		public bool CanFire
		{
			get
			{
				foreach (Transition trans in transitions)
				{
					if (trans.CanFire)
						return true;
				}
				return false;
			}
		}

		/// <summary>
		/// Fire all transitions that can be fired
		/// </summary>
		/// <preconditions>
		/// IsRunning must be true
		/// CanFire must be true
		/// </preconditions>
		public void FireAllTransitions()
		{
			#region preconditions
			Debug.Assert(IsRunning, "FireAllTransitions called when IsRunning is false",
				"PetriNetGraph had FireAllTransitions() called when IsRunning is false");
			Debug.Assert(CanFire, "FireAllTransitions called when CanFire is false",
				"PetriNetGraph had FireAllTransitions() called when CanFire is false");
			#endregion

			if (AboutToFire != null)
				AboutToFire();

			foreach (Transition trans in transitions)
			{
				if (trans.CanFire)
					trans.Fire();
			}

			if(Fired != null)
				Fired();
		}

		#endregion

		#region unique ID functionality

		/// <summary>
		/// Generate a unique ID for a Transition
		/// </summary>
		/// <returns>A unique ID for a Transition</returns>
		protected string GenerateTransitionUID()
		{
			string UID = "";
			bool unique = false;
			while (!unique)
			{
				unique = true;
				UID = TRANSITION_UID_PREFIX + new Random().Next(MAX_UID_NUMBER).ToString();

				foreach (Transition trans in transitions)
					unique = unique && !trans.UID.Equals(UID);
			}

			return UID;
		}

		/// <summary>
		/// Generate a unique ID for a Place
		/// </summary>
		/// <returns>A unique ID for a Place</returns>
		protected string GeneratePlaceUID()
		{
			string UID = "";
			bool unique = false;
			while (!unique)
			{
				unique = true;
				UID = PLACE_UID_PREFIX + new Random().Next(MAX_UID_NUMBER).ToString();

				foreach (Place place in places)
					unique = unique && !place.UID.Equals(UID);
			}
			return UID;
		}

		/// <summary>
		/// Get a transition by its unique ID
		/// </summary>
		/// <param name="UID">The unique ID of the transition</param>
		/// <returns>The transition belonging to the unique ID, null if none is found</returns>
		public Transition GetTransition(string UID)
		{
			foreach (Transition trans in transitions)
			{
				if (trans.UID.Equals(UID))
					return trans;
			}
			return null;
		}

		/// <summary>
		/// Get a place by its unique ID
		/// </summary>
		/// <param name="UID">The unique ID of the place</param>
		/// <returns>The place belonging to the unique ID, null if none is found</returns>
		public Place GetPlace(string UID)
		{
			foreach (Place place in places)
			{
				if (place.UID.Equals(UID))
					return place;
			}
			return null;
		}

		/// <summary>
		/// Get whether the unique ID passed is a unique ID for a transition
		/// </summary>
		/// <param name="UID">The unique ID to check</param>
		/// <returns>True if it is, false if not</returns>
		public static bool IsTransitionUID(string UID)
		{
			if (UID.StartsWith(TRANSITION_UID_PREFIX))
				return true;
			return false;
		}

		/// <summary>
		/// Get whether the unique ID passed is a unique ID for a place
		/// </summary>
		/// <param name="UID">The unique ID to check</param>
		/// <returns>True if it is, false if not</returns>
		public static bool IsPlaceUID(string UID)
		{
			if (UID.StartsWith(PLACE_UID_PREFIX))
				return true;
			return false;
		}

		#endregion
	}
}
